<?php
###########################################################################
#  Copyright (C) 2006-2007 Glyphix, Inc. <briz@glyphix.com>               #
#                                                                         #
#  Permission is hereby granted, free of charge, to any person obtaining  #
#  a copy of this software and associated documentation files (the        #
#  "Software"), to deal in the Software without restriction, including    #
#  without limitation the rights to use, copy, modify, merge, publish,    #
#  distribute, sublicense, and/or sell copies of the Software, and to     #
#  permit persons to whom the Software is furnished to do so, subject to  #
#  the following conditions:                                              #
#                                                                         #
#  The above copyright notice and this permission notice shall be         #
#  included in all copies or substantial portions of the Software.        #
#                                                                         #
#  Except as contained in this notice, the name(s) of the above           #
#  copyright holders shall not be used in advertising or otherwise to     #
#  promote the sale, use or other dealings in this Software without       #
#  prior written authorization.                                           #
#                                                                         #
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        #
#  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     #
#  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. #
#  IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR      #
#  OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,  #
#  ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR  #
#  OTHER DEALINGS IN THE SOFTWARE.                                        #
###########################################################################

/**
 * GXPayPal
 *
 * Performs PayPal Pro payments using PayPal's SOAP interface.
 * Uses some code provided by http://www.framewerk.org/ to build the SOAP message.
 * @author		Brad Brizendine <briz@glyphix.com>
 * @link		http://www.glyphix.com/
 * @license		http://opensource.org/licenses/bsd-license.php BSD License
 * @version		1.0
 * @package		GXPage
 * @subpackage	Engine
 * @copyright	Copyright 2006 - 2007, Glyphix, Inc.
 * @uses		GXModule
 */

class GXPayPal extends GXModule {

	public $account;	// the name of the paypal account
	public $config;		// the config provided from System.xml
	public $apiconfig;	// the <API> node

	// paypal api settings
	public $mode;
	public $version;
	public $timeout;
	public $livewsdl;
	public $sandboxwsdl;

	// paypal account settings
	public $username;
	public $password;
	public $signature;

	// soap object
	public $soap;
	public $soapHeader;

	// various data types
	public $Payment;
	public $CreditCard;

	// init GXModule and process conf
	public function __construct( $config = null ){
		parent::__construct();

		// get the paypal <API> node ... have to use a top-level xpath in case config isn't xml
		$this->apiconfig = $this->GXPage->SystemConfig->xpath('//Modules/PayPal/API',0);

		// get the settings from the provided arg
		switch(true){
			// if we don't have anything for config, get the first item
			case !$config:
				// grab the first non-API node as the default account
				$config = $this->GXPage->SystemConfig->xpath('//Modules/PayPal/*[ not(name()="API") ][1]',0)->nodeName;
				// don't break so we just continue with is_string
			// if config is a string, treat it as a paypal account name
			case is_string($config):
				$this->account = $config;
				// get the named paypal account by name
				$xpath = '//Modules/PayPal/*[ name() = "' .$this->account .'" ]';
				$this->config = $this->SystemConfig->xpath($xpath,0);
				// determine the mode
				$this->mode = $this->config->attributes['Mode'];
				// set config to the mode within the selected account
				$this->config = $this->config->exec->xpath($this->mode,0);
				break;
			// if it's an xml object, treat it as the config from SystemConfig
			case get_class($config) == 'GXDOM':
				// grab the first node as the default account
				$this->config = $config->exec->xpath('*[ not(name()="API") ][1]',0);
				$this->account = $this->config->nodeName;
				if( !$this->account ){
					trigger_error( 'Bad format in PayPal config in System.xml', E_USER_ERROR );
					return false;
				}
				// determine the mode
				$this->mode = $this->config->attributes['Mode'];
				// set config to the mode within the selected account
				$this->config = $this->config->exec->xpath($this->mode,0);
				break;
			// otherwise, we have an error
			default:
				trigger_error( 'Unable to parse config in PaymentCore constructor', E_USER_ERROR );
				return false;
		}

		// get the node
		$this->version		= $this->apiconfig->exec->xpath('Version',0)->nodeValue;
		$this->timeout		= $this->apiconfig->exec->xpath('Timeout',0)->nodeValue;
		/*
		$this->livewsdl		= $this->apiconfig->exec->xpath('LiveWSDL',0)->nodeValue;
		$this->sandboxwsdl	= $this->apiconfig->exec->xpath('SandboxWSDL',0)->nodeValue;
		*/
		$this->livewsdl		= dirname( __FILE__ ) .'/Data/GXPayPal/live/PayPalSvc.wsdl';
		$this->sandboxwsdl	= dirname( __FILE__ ) .'/Data/GXPayPal/sandbox/PayPalSvc.wsdl';

		// set up the soap client
		$this->username		= $this->config->exec->xpath('Username',0)->nodeValue;
		$this->password		= $this->config->exec->xpath('Password',0)->nodeValue;
		$this->signature	= $this->config->exec->xpath('Signature',0)->nodeValue;
		$this->url = (strtolower($this->mode) == 'live') ? $this->livewsdl : $this->sandboxwsdl;

		// instantiate the soap stuff
		$this->soap = @new SoapClient( $this->url, array( 'soap_version' => SOAP_1_1, 'exceptions' => true, 'trace' => true, 'connection_timeout ' => $this->timeout ) );
 		$this->soapHeader = new SoapHeader(
			'urn:ebay:api:PayPalAPI',
			'RequesterCredentials',
			array(
				'Credentials'=> new SoapVar( array('Username'=> $this->username, 'Password'=> $this->password, 'Signature'=> $this->signature ),
				SOAP_ENC_OBJECT,
				null,
				null,
				null,
				'urn:ebay:apis:eBLBaseComponents' )
				)
			);

		// initialize main api data types
		// if we don't, classes instantiating this one will not be able to __set values
		$this->Payment = new PayPalPaymentDetails();
		$this->CreditCard = new PayPalCreditCardDetails();

		// load country list for state/province validation
		$this->StateOrProvince = new GXDOM('countries.xml');
		return true;
	}

	/**
	 * __call
	 * Magic function to send the function call to the soap object.
	 * @param string $function
	 * @param array $vals
	 * @return mixed
	 */
	function __call( $function = null, $vals = null ){
		// add the API Version to the array of vals
		$vals = array_merge($vals[0], array('Version'=>$this->version));
		// format params
		$params = array( $function .'Request'=>$vals );
		// perform the call
		try{
            $result = $this->soap->__soapCall( $function, array($params), null, $this->soapHeader );
        }catch( SoapFault $Exception ){
			$result = $Exception;
        }
		// stick the param on the end
		$result->data = $params;
		return $result;
	}

	/**
	 * getTransaction
	 * @param string $transactionID
	 * @return mixed
	 */
	function getTransaction( $transactionID = null ){
		return $this->__call( 'GetTransactionDetails', array('TransactionID'=>$transactionID) );
	}

	/**
	 * runPayment
	 * Runs a DoDirectPayment operation and returns PayPal's response.
	 * @param string $action either Sale or Authorization
	 * @param string $ip the ip address of the customer
	 * @param string $merchantSessionID the merchant's tracking session id
	 * @return object or false on failure
	 */
	public function runPayment( $action = 'Sale', $ip = null, $merchantSessionID = null ){
		if( !(strtolower($action) == 'sale' || strtolower($action) == 'authorization') ){
			trigger_error( 'Invalid payment action: ' .$action, E_USER_ERROR );
			return false;
		}
		$action = ucwords($action);
		// default values
		if( !$ip ){
			$ip = $_SERVER['REMOTE_ADDR'];
		}
		if( !$merchantSessionID ){
			$merchantSessionID = md5(time());
		}

		// collect values
		$vals = array(
			'IPAddress'			=> $ip,
			'MerchantSessionID'	=> $merchantSessionID,
			'PaymentAction'		=> $action,
			'CreditCard'		=> $this->CreditCard->getArrayValues(),
			'PaymentDetails'	=> $this->Payment->getArrayValues()
			);
		// check for errors
		if( $err = PayPalValidation::getErrors() ){
			// load each validation error into E_USER_NOTICE
			foreach( $err as $txt ){
				trigger_error( $txt, E_USER_NOTICE );
			}
			return false;
		}
		return $this->DoDirectPayment( array('DoDirectPaymentRequestDetails'=>$vals) );
	}

	// express checkout requires 3 functions: SetExpressCheckout, GetExpressCheckoutDetails, DoExpressCheckoutPayment
	public function setExpress(){
		$data = new PayPalSetExpressCheckoutRequestDetails();
		$data = $data->getArrayValues();
		return $this->SetExpressCheckout( array('SetExpressCheckoutRequestDetails'=>$data) );
	}
	public function getExpress( $token = null ){
		if( !$token ){
			trigger_error( 'Missing token', E_USER_ERROR );
			return false;
		}
		$data = $token;
		return $this->GetExpressCheckoutDetails( array('Token'=>$data) );
	}
	public function runExpress( $token = null ){
		if( !$token ){
			trigger_error( 'Missing token', E_USER_ERROR );
			return false;
		}
		$pmt = new PayPalDoExpressCheckoutPayment();
		$data = array(
			'Token'				=> $token,
			'PaymentAction'		=> $pmt->PaymentAction,
			'PayerID'			=> $pmt->PayerID,
			'PaymentDetails'	=> $pmt->PaymentDetails->getArrayValues()
			);
		return $this->DoExpressCheckoutPayment( array('Token'=>$vals) );
	}

}

/**
 * PayPalValidation
 *
 * Stores validation errors when collecting paypal types.
 */
class PayPalValidation {

	public static $err = array();

	public static function msg( $txt = null ){
		if( !$txt ){
			trigger_error( 'Missing txt', E_USER_ERROR );
			return false;
		}
		self::$err[] = $txt;
	}

	public static function getErrors(){
		return self::$err;
	}

}

/**
 * PayPalDataElement
 *
 * Base class for all PayPal types.
 * It stores and retrieves named values, formatting them according to the PayPal spec.
 */
class PayPalDataElement {
	private $Fields;		// array of collected fields
	private $ClassName;		// name of the class, which translates to a soap element
	private $Required;		// determines if the soap element is required
	private $Type;			// the type of the element
	private $Values;		// array of values

	/**
	 * getArrayValues
	 * Runs through all values collected for this class and formats them appropriately.
	 * @param boolean $required
	 * @return array or false on failure
	 */
	function getArrayValues( $required = true ){
		// stores collected values
		$value = array();
		// increments each time a field is added to the $value array; if zero at end, no fields
		$total = 0;
		// loop thru this object's Fields array
		foreach( $this->Fields as $field ){
			// if it's required, make sure we have something
			if( ( in_array($field, $this->Required) ) && ($required == true) ){
				if( !isset($this->Values[$field]) ){
					PayPalValidation::msg('Required field not set in ' .str_replace('PayPal','',get_class($this)) .' (' .$this->ClassName .'::' .$field .')');
					trigger_error( 'Required field not set (' .$field .')', E_USER_ERROR );
					// don't return false so that we collect all errors at once
				}
				// if it's not an object, see if it's a string
				if( !is_object($this->Values[$field]) ){
					// if it's not a string, it's an error
					if( !strlen($this->Values[$field]) ){
						PayPalValidation::msg('Required field not set in ' .str_replace('PayPal','',get_class($this)) .' (' .$this->ClassName .'::' .$field .')');
						trigger_error( 'Required field ' .$this->ClassName .'::' .$field .' is not set', E_USER_ERROR );
						// don't return false so that we collect all errors at once
					}
				}
			}
			// if we have something and it's an object
			if( isset($this->Values[$field]) && is_object($this->Values[$field]) ){
				$required = false;
				if( in_array($field, $this->Required) ){
					$required = true;
				}
				// add this field to the list if it returns something
				$temp = $this->Values[$field]->getArrayValues($required);
				$total++;
				if( $temp ){
					$value[$field] = $temp;
				}
			}else{
				// if it's not set, continue
				if( !isset($this->Values[$field]) ){
					trigger_error( 'Field ' .$field .' is not set', E_USER_WARNING );
					continue;
				}
				// if it's an array, it's invalid, so continue
				if( is_array($this->Values[$field]) && !$this->Values[$field] ){
					trigger_error( $field .' is an empty array', E_USER_WARNING );
					continue;
				}
				$len = strlen($this->Values[$field]);
				$total++;
				// if we're here, we have a value, so process it according to its type
				if( $len ){
					// get the field's type: "Currency" or not
					$type = null;
					if( isset($this->Type[$field]) ){
						$type = $this->Type[$field];
					}
					switch($type){
						case 'Currency':
							// weird: the value's name is an underscore, and there needs to be a <currencyID> node
							$value[$field] = array('_'=>number_format($this->Values[$field],2),'currencyID'=>'USD');
							break;
						default;
							$value[$field] = $this->Values[$field];
							break;
					}
				}
			}
		}
		// if we have a total, return the value array
		if( $total ){
			return $value;
		}else{
			return false;
		}
	}

	function __get( $field = null ){
		if( in_array($field, array( 'Fields', 'Required', 'ClassName', 'Type' )) ){
			return $this->$field;
		}elseif( in_array($field, $this->Fields) ){
			return $this->Values[$field];
		}
	}

	function __set( $field = null, $value = null ){
		if( in_array($field, array( 'Fields', 'Required', 'ClassName', 'Type' )) ){
			$this->$field = $value;
		}elseif( in_array($field, $this->Fields) ){
			$this->Values[$field] = $value;
		}
	}
}

class PayPalAddress extends PayPalDataElement {

	function __construct( $name = null ){
		$this->Fields			= array( 'CityName', 'Country', 'PostalCode', 'StateOrProvince', 'Street1', 'Street2', 'Name' );
		$this->Required			= array( 'Street1', 'CityName', 'StateOrProvince', 'Country', 'PostalCode' );
		$this->ClassName		= $name;
	}

	function __set( $field = null, $value = null ){
		// if it's country, validate the state against it
		if( $field == 'Country' ){
			if( !GXValidator::state($value,$this->StateOrProvince) ){
				PayPalValidation::msg('Invalid state for this country in ' .str_replace('PayPal','',get_class($this)) .' (' .$value .',' .$this->StateOrProvince .')');
				trigger_error( 'Invalid state for this country (' .$value .',' .$this->StateOrProvince .')', E_USER_ERROR );
				return false;
			}
		}
		parent::__set( $field, $value );
	}

}

class PayPalShipToAddress extends PayPalAddress {

	function __set( $field = null, $value = null ){
		return parent::__set( $field, $value );
	}

}

class PayPalCreditCardDetails extends PayPalDataElement {

	function __construct( $name = null ){
		$this->ClassName		= $name;
		$this->Fields			= array( 'CreditCardType', 'CreditCardNumber', 'ExpMonth', 'ExpYear', 'CVV2', 'CardOwner' );
		$this->Required			= array( 'CreditCardType', 'CreditCardNumber', 'ExpMonth', 'ExpYear', 'CVV2', 'CardOwner' );
		$this->CardOwner		= new PayPalPayerInfo('CardOwner');
	}

	function __set( $field = null, $value = null ){
		if( $field == 'CreditCardNumber' ){
			// make sure the credit card number has numbers only
			$value = ereg_replace('[^0-9]+', '', $value);
			// determine the credit card type by examining the first digit
			$start = substr($value, 0, 1);
			switch ( $start ){
				case '3':
					$this->CreditCardType = 'Amex';
					break;
				case '4':
					$this->CreditCardType = 'Visa';
					break;
				case '5':
					$this->CreditCardType = 'MasterCard';
					break;
				case '6':
					$this->CreditCardType = 'Discover';
					break;
				default:
					trigger_error( 'Invalid card number first digit: ' .$start, E_USER_ERROR );
					return false;
			}
		}
		parent::__set( $field, $value );
	}

}

class PayPalPayerInfo extends PayPalDataElement{

	function __construct( $name = null ){
		$this->ClassName		= $name;
		$this->Fields			= array( 'Payer', 'PayerName', 'Address' );
		$this->Required			= array( 'PayerName', 'Address' );
		$this->PayerName		= new PayPalPayerName( 'PayerName' );
		$this->Address			= new PayPalAddress('Address');
	}

}

class PayPalPayerName extends PayPalDataElement {

	function __construct( $name = null ){
		$this->ClassName		= 'PayerName';
		$this->Fields			= array( 'FirstName', 'LastName' );
		$this->Required			= array( 'FirstName', 'LastName' );
	}

}

class PayPalPaymentDetails extends PayPalDataElement {

	function __construct( $name = null ){
		$this->ClassName = $name;
		$this->Fields = array(
			'ButtonSource',
			'Custom',
			'HandlingTotal',
			'InvoiceID',
			'ItemTotal',
			'NotifyURL',
			'OrderDescription',
			'OrderTotal',
			'PaymentDetailsItem',
			'ShippingTotal',
			'ShipToAddress',
			'TaxTotal'
			);
		$this->Required = array('OrderTotal');
		$this->Type = array(
			'OrderTotal'=>'Currency',
			'ItemTotal'=>'Currency',
			'HandlingTotal'=>'Currency',
			'ShippingTotal'=>'Currency',
			'TaxTotal'=>'Currency'
			);

		$this->PaymentDetailsItem = new PayPalPaymentDetailsItem('PaymentDetailsItem');
		$this->ShipToAddress = new PayPalShipToAddress('ShipToAddress');
	}

}

class PayPalPaymentDetailsItem extends PayPalDataElement {

	function __construct( $classname = null ){
		$this->ClassName		= $classname;
		$this->Fields			= array( 'Name', 'Amount', 'Number', 'Quantity', 'Tax' );
		$this->Type				= array( 'Amount'=>'Currency', 'Tax'=>'Currency' );
		$this->Required			= array();
	}

}

class PayPalSetExpressCheckoutRequestDetails extends PayPalDataElement {

	function __construct( $name = null ){
		$this->ClassName = $name;
		$this->Fields   = array(
			'OrderTotal',
			'ReturnURL',
			'CancelURL',
			'Token',
			'MaxAmount',
			'OrderDescription',
			'Custom',
			'InvoiceID',
			'ReqConfirmShipping',
			'NoShipping',
			'AddressOverride',
			'LocaleCode',
			'PageStyle',
			'cpp-header-image',
			'cpp-header-border-color',
			'cpp-header-back-color',
			'cpp-payflow-color',
			'Address',
			'PaymentAction',
			'BuyerEmail'
			);
		$this->Required			= array( 'OrderTotal', 'ReturnURL', 'CancelURL' );
		$this->Type				= array( 'OrderTotal'=>'Currency', 'MaxAmount'=>'Currency' );
		$this->Address			= new PayPalAddress('Address');
	}

	function __set( $field = null, $value = null ){
		parent::__set($field, $value);
	}

}

class PayPalDoExpressCheckoutPayment extends PayPalDataElement {

	function __construct( $name = null ){
		$this->ClassName		= $name;
		$this->Fields			= array( 'Token', 'PaymentAction', 'PayerID', 'PaymentDetails' );
		$this->Required			= array( 'Token', 'PaymentAction', 'PayerID', 'PaymentDetails' );
		$this->PaymentDetails	= new PayPalPaymentDetails('PaymentDetails');
	}

}

?>